(function() {
	/* mouse pointer's last cordinates and distances moved */
	var _lastX, _lastY, _accuX, _accuY;

	/* threshold for leaned (like "left up") strokes
	 * this feature is on hold because the matching rate is low */
	var _lean = 1 / Math.tan(30 * Math.PI / 180);

	/* the stroking event object, will be passed to the stroke handler */
	var _event;

	/* the stroke movement's end time */
	var _endtm;

	/* true if the last regular/rocker/wheel stroke is fired */
	var _fired;

	/* true if this script is embeded in the extension's option page */
	var _optpg = OPTION_URL === location.href;

	/* the <canvas> element and its context for trail drawing */
	var _tcanvas, _tcontex;

	/* the connection port between the backend page */
	var _port = chrome.extension.connect({name: 'stroke.html-stroke'});

	/* configurations */
	var _cfg = {
		/* default values */
		trigger: RBUTTON,
		timeout: 1000,
		minstep: 20,
		trail: '',
		suppress: true
	}

	/* global variable container */
	var _heap = {
		/* status of left and right mouse buttons,
		 * they are global because rocker or wheel strokes can be
		 * used to navigate between tabs, they depend on previous
		 * tabs' mouse button status to behave continuously */
		ldown: false, rdown: false,
		/* whether a rocker or wheel stroke has just been fired,
		 * the reason of making them global is similar to ldown/rdown,
		 * based on previous tab's  the last selected tab need to decide if the context menu
		 * should be suppressed or not */
		rkfired: false, mwfired: false
	}

	/* calculate weights by distances moved in both directions */
	var weigh = function(x, y) {
		return Math.round(Math.sqrt(x * x + y * y));
	}

	/* search for image that is possibly positioned at the given
	 * element's coordinate */
	var imgsrc = function(elem) {
		var src;
		if (!elem)
			return null;
		else if ('IMG' == elem.nodeName && !!elem.src)
			return elem.src;
		else if (src = getComputedStyle(elem).getPropertyValue('background-image'))
			/* getPropertyValue() returns string literal 'none' if
			 * no background images are set */
			return src.match(/^url\(.+\)$/) && src.replace(/^url\((.+)\)$/, '$1');
		else return imgsrc(elem.parentNode);
	}

	/* strip MouseEvent object to void 'Converting circular structure to
	 * JSON' errors when converting it to JSON string */
	var strip = function(ev) {
		/* keep useful properties only, also retain the MouseEvent
		 * object structure */
		return {
			/* rocker strokes need this to know which button is
			 * pressed first */
			button: ev.button,
			srcElement: {
				/* link type strokes need these properties */
				textContent: ev.srcElement.textContent,
				href: ev.srcElement.href,
				/* image type strokes need these */
				src: imgsrc(ev.srcElement),
				/* they all got this */
				nodeName: ev.srcElement.nodeName
			}
		};
	}

	/* create the canvas element for trail drawing */
	var canvas = function() {
		_tcanvas = $.create('canvas', {
			width: window.innerWidth, height: window.innerHeight
		});
		_tcanvas.style.cssText = 'left:0;position:fixed;top:0;z-index:' + new Date().getTime() + ';';
		_tcontex = _tcanvas.getContext('2d');
		/* styling */
		_tcontex.globalAlpha = 0.3;
		_tcontex.lineWidth = 3;
		_tcontex.lineCap = 'round';
		_tcontex.shadowOffsetX = 0;
		_tcontex.shadowOffsetY = 0;
		_tcontex.shadowBlur = 3;
		_tcontex.shadowColor = _cfg.trail;
		_tcontex.strokeStyle = _cfg.trail;
	}

	/* set global variable */
	var setg = function(name, value) {
		_heap[name] = value;
		/* tell background page to update */
		_port.postMessage({ type: 'set-global',
			data: { name: name, value: value }
		});
	}

	/* get global variable */
	var getg = function(name) {
		return _heap[name];
	}

	/* assemble a stroke event, NOTE: all types of strokes, including
	 * rockers and wheels, will use this event */
	var genevt = function(ev) {
		return {
			/****** general properties ******/
			/* stroke, rocker or wheel */
			type: null,
			/* the initial event (mousedown for all 3 types) */
			initevt: strip(ev),
			/* the finish event (mouseup for stroke type, mousedown
			 * for rocker type and mousewheel for wheel type) */
			finevt: null,
			/* currently selected text */
			seltext: window.getSelection().toString(),

			/****** stroke type properties ******/
			/* stroke array */
			stroke: [],
			/* weights of each stroke part in the stroke array */
			weights: [],
			/* is this a user defined stroke? */
			custom: ev.ctrlKey,

			/****** wheel type properties ******/
			/* wheel direction, UP, DOWN, LEFT or RIGHT */
			wheel: null,

			/****** rocker type properties ******/
			/* rocker direction, LEFT -> RIGHT or RIGHT -> LEFT */
			rocker: null
		};
	}

	/* test if required keys of regular strokes are (not) pressed */
	var stroke_keys = function(ev) {
		return ev.button == _cfg.trigger &&
			(!_cfg.suppress || !ev.altKey && !ev.metaKey);
	}

	/* initate a regular or drag stroke */
	var stroke_init = function(ev, drag) {
		if (!drag && _cfg.trail) {
			/* prepare to draw trails */
			if (!_tcontex) canvas();
			_tcontex.beginPath();
			_tcontex.moveTo(ev.clientX, ev.clientY);
		}

		/* start recording path, reset everything */
		_lastX = ev.screenX;
		_lastY = ev.screenY;
		_accuX = _accuY = 0;

		/* disable auto-scroll to support strokes on Linux and Mac */
		if (MBUTTON == ev.button) ev.preventDefault();
	}

	/* track mouse movements in a regular or drag stroke transaction */
	var stroke_move = function(ev, drag) {
		var move, last_idx,
			x = ev.screenX, y = ev.screenY,
			offsetX = x - _lastX, offsetY = y - _lastY,
			absX = Math.abs(offsetX), absY = Math.abs(offsetY);

		/* the movement is negligible */
		if (absX < _cfg.minstep && absY < _cfg.minstep) return;

		if (!drag && _cfg.trail) {
			/* BUG=11
			 * insert the canvas into the DOM tree now, other
			 * than on the moment when it was created, because
			 * operations on <body> will cause any selected
			 * elements to lose focus */
			if (!_tcanvas.appended) {
				document.body.insertBefore(_tcanvas,
					document.body.firstChild);
				_tcanvas.appended = true;
			}
			_tcontex.lineTo(ev.clientX, ev.clientY);
			_tcontex.stroke();
		}
		_lastX = x;
		_lastY = y;
		_accuX += absX;
		_accuY += absY;

		/* ignore leaning, blurrer strokes are more recognizable */
//		if (0 == absY || (absX / absY > _lean)) {
//		} else if (0 == absX || (absY / absX > _lean)) {
//		} else {
//			move = (offsetX > 0 ? RIGHT : LEFT)
//				+ (offsetY > 0 ? DOWN : UP);
//		}
		if (absX > absY) {
			move = offsetX > 0 ? RIGHT : LEFT;
		} else {
			move = offsetY > 0 ? DOWN : UP;
		}

		last_idx = _event.stroke.length - 1;
		if (-1 == last_idx || _event.stroke[last_idx] != move) {
			/* first move or direction changed */
			_accuX = absX;
			_accuY = absY;
			_event.stroke.push(move);
			_event.weights.push(weigh(_accuX, _accuY));
			$.notify('[' + _event.stroke.join(' ') + ']', 2);
		} else {
			/* update weights */
			_event.weights[last_idx] = weigh(_accuX, _accuY);
		}
		/* update movement's ending time */
		_endtm = new Date().getTime();
	}

	/* fire a regular or drag stroke */
	var stroke_fire = function(ev, drag) {
		/* trail should be cleared even if it's not a valid stroke */
		if (!drag && _cfg.trail) {
			_tcontex.clearRect(0, 0, _tcanvas.width, _tcanvas.height);
			if (_tcanvas.appended) {
				document.body.removeChild(_tcanvas);
				_tcanvas.appended = false;
			}
		}

		if (!_event || !_event.stroke.length ||
			(new Date().getTime() - _endtm) > _cfg.timeout) {
			/* no strokes or timeouted */
			return;
		}

		/* it's a valid stroke */
		_fired = true;
		_event.type = !drag ? 'stroke' : 'drag';
		_event.mouseup = strip(ev);

		if (!drag && _optpg) {
			/* post event for options.html */
			var e = document.createEvent('Event');
			e.initEvent('set-stroke-event', false, false);
			e.event = _event;
			document.dispatchEvent(e);
		} else if (!_optpg) {
			/* post regular strokes */
			_port.postMessage({ type: 'stroke', data: _event });
		}
		ev.preventDefault();
	}

	/* test if required keys of regular strokes are (not) pressed */
	var drag_keys = function(ev) {
		return ev.button == LBUTTON &&
			(!_cfg.suppress || !ev.altKey && !ev.metaKey);
	}

	/* initiate drag'n'drop stroke */
	var drag_init = function(ev) {
		/* only links can be draged */
		var el = ev.srcElement;
		while (!!el && el.tagName != 'A') el = el.parentNode;
		if (!!el) {
			ev.srcElement.href = el.href;
			stroke_init(ev, true);
			_event.type = 'drag';
			ev.preventDefault();
		}
	}

	/* track drag direction */
	var drag_move = function(ev) {
		/* remember the first drag direction only */
		if ('drag' == _event.type && _event.stroke.length < 1)
			stroke_move(ev, true);
	}

	/* fire a drag stroke */
	var drag_fire = function(ev) {
		/* accept the first drag direction */
		_event.drag = _event.stroke[0];
		stroke_fire(ev, true);
	}

	/* fire a rocker */
	var rocker_fire = function(ev) {
		var dir;

		if (!WIN) {
			setg('ldown', false), setg('rdown', false);
			return;
		}

		/* when navigating between tabs by rocker or wheel strokes,
		 * _event can be undefined because the mousedown events is
		 * fired in another tab */
		if (!_event) {
			_event = genevt(ev);
			/* the other button is pressed first */
			dir = LBUTTON == ev.button ? LEFT : RIGHT;
		} else {
			dir = LBUTTON == _event.initevt.button ? RIGHT : LEFT;
		}
		_event.type = 'rocker';
		_event.finevt = strip(ev);
		_event.rocker = dir;
		setg('rkfired', true);
		ev.preventDefault();
		_port.postMessage({ type: 'stroke', data: _event });
	}

	/* fire a wheel */
	var wheel_fire = function(ev) {
		if (!WIN) {
			setg('ldown', false), setg('rdown', false);
			return;
		}

		if (!_event) _event = genevt(ev);

		_event.type = 'wheel';
		_event.finevt = strip(ev);
		_event.wheel = ev.wheelDeltaY < 0 ? DOWN :
				ev.wheelDeltaY > 0 ? UP :
				ev.wheelDeltaX < 0 ? RIGHT :
				ev.wheelDeltaX > 0 ? LEFT : null;
		setg('mwfired', true);
		ev.preventDefault();
		_port.postMessage({ type: 'stroke', data: _event });
	}

	/* suppress the context menu when the trigger is right mouse button */
	window.addEventListener('contextmenu', function(ev) {
		/* right button is bound to be involved in both rocker and
		 * wheel strokes, after firing such strokes, a contextmenu
		 * should always been killed */
		if (getg('rkfired') || getg('mwfired') ||
			_fired && _cfg.trigger == RBUTTON) {
			ev.preventDefault();
			/* the point of all *fired variables is to prevent
			 * context menu, so reset them all after suppression */
			_fired = false;
			setg('rkfired', false);
			setg('mwfired', false);
		}
	}, true);

	/* detect wheel strokes */
	window.addEventListener('mousewheel', function(ev) {
		if (getg('rdown') && (!_event || !_event.stroke.length))
			wheel_fire(ev);
	}, true);

	/* initiate stroke events */
	window.addEventListener('mousedown', function(ev) {
		if (LBUTTON == ev.button) {
			setg('ldown', true);
			if (!getg('rdown')) _event = genevt(ev);
		} else if (RBUTTON == ev.button) {
			setg('rdown', true);
			if (!getg('ldown')) _event = genevt(ev);
		} else {
			_event = genevt(ev);
		}

		if (!!_event && !getg('rkfired') && !getg('mwfired')) {
			if (stroke_keys(ev)) stroke_init(ev);
			else if (drag_keys(ev)) drag_init(ev);
		}
	}, true);

	/* track mouse movements */
	window.addEventListener('mousemove', function(ev) {
		if (!!_event && !getg('rkfired') && !getg('mwfired') &&
			/* add this condition because on mousemove events, the
			 * event.button is 0 either when the left button is
			 * pressed or not, there is no way to tell if the left
			 * button is pressed.
			 * www.quirksmode.org/js/events_properties.html */
			(getg('ldown') || LBUTTON != _cfg.trigger)) {

			if (stroke_keys(ev)) stroke_move(ev);
			else if (drag_keys(ev)) drag_move(ev);
		}
	}, true);

	/* finish stroking */
	window.addEventListener('mouseup', function(ev) {
		/* sometimes mousedown is not fired, this is a workground */
		if (RBUTTON == ev.button && !getg('rdown')) setg('rdown', true);
		/* rockers are fired if both mouse buttons were pressed and
		 * the mouse was not moved since last mousedown event */
		var rkfired = getg('ldown') && getg('rdown') &&
			(!_event || !_event.stroke.length);

		if (LBUTTON == ev.button) setg('ldown', false);
		else if (RBUTTON == ev.button) setg('rdown', false);

		if (rkfired) {
			rocker_fire(ev);
		} else if (!!_event && !getg('rkfired') && !getg('mwfired') &&
			!!_event.stroke.length) {

			if (stroke_keys(ev)) stroke_fire(ev);
			else if (drag_keys(ev)) drag_fire(ev);
		}
		_event = undefined;
	}, true);

	/* response to requests from background page */
	chrome.extension.onRequest.addListener(function(msg, sender, resp) {
		if ('update-config' == msg.type) {
			var script;

			_cfg = msg.data;
			if (!_cfg.locale) return;

			script = $.create('script', {
				type: 'text/javascript',
				src: chrome.extension.getURL('locales/' + _cfg.locale + '.json')
			});
			script.addEventListener('load', function() {
				/* i18n is initiated, if the locale file
				 * doesn't exist, this event will NOT fire */
				console.log('Locale %s is loaded', _cfg.locale);
			});
			document.head.appendChild(script);
		} else if ('local-eval' == msg.type) {
			var ex, id = msg.data.id, tab = msg.data.tab,
				event = msg.data.event,
				custom = msg.data.custom,
				script = msg.data.script;

			try {
				if (custom) {
					/* event is passed as the magic _env
					 * variable to the sandbox */
					ex = sandbox.call(tab, event, script);
					if (true !== ex) throw ex;
				} else {
					_actions[id].call(tab, tab, event);
				}
			} catch (ex) {
				console.error(ex);
			}
		} else if ('put-globals' == msg.type) {
			_heap = msg.data;
		} else if ('show-message' == msg.type) {
			$.notify(msg.data.html, msg.data.duration);
		}
		try { resp({}) } catch (e) {}
	});

	/* query config from the background page */
	_port.postMessage({ type: 'pull-config' });
	_port.postMessage({ type: 'get-globals' });
})();
